#include "shader_catalog.h"
#include "shader.h"
#include "matrix.h"
#include "vector3.h"
#include "renderer.h"
#include "timer.h"

using namespace krryn::math;
using namespace krryn::view;

namespace{
	/**
	 * Shader param naming convention:
	 *  m = model
	 *  v = view
	 *  p = projection
	 *  i = inverse (only as prefix, of imv is possible but mvi of miv aren't)
	 */

	// TODO: Automatically bind any combination of those letters

	class shader_auto_parameter_mvp : public shader_auto_parameter{
	public:
		shader_auto_parameter_mvp(renderer &a_Renderer) : m_Renderer(a_Renderer) {}

		virtual std::string get_name() { return "mvp"; }

		virtual void configure_shader(shader_type a_Type, shader &a_Shader){
			matrix mvp(m_Renderer.get_matrix_stack(matrix_modelview).top());
			mvp.concatenate(m_Renderer.get_matrix_stack(matrix_projection).top());

			a_Shader.bind_matrix4x4(a_Type, get_name(), mvp);
		}
	private:
		renderer &m_Renderer;
	};

	class shader_auto_parameter_mv : public shader_auto_parameter{
	public:
		shader_auto_parameter_mv(renderer &a_Renderer) : m_Renderer(a_Renderer) {}

		virtual std::string get_name() { return "mv"; }

		virtual void configure_shader(shader_type a_Type, shader &a_Shader){
			a_Shader.bind_matrix4x4(a_Type, get_name(), m_Renderer.get_matrix_stack(matrix_modelview).top());
		}
	private:
		renderer &m_Renderer;
	};

	class shader_auto_parameter_time : public shader_auto_parameter{
	public:
		shader_auto_parameter_time() { m_Timer.begin(); }

		virtual std::string get_name() { return "time"; }

		virtual void configure_shader(shader_type a_Type, shader &a_Shader){
			a_Shader.bind_float(
				a_Type, 
				get_name(), 
				m_Timer.duration_in_seconds());
		}
	private:
		krryn::core::timer m_Timer;
	};

	class shader_auto_parameter_krryn_error_colors : public shader_auto_parameter{
	public:
		shader_auto_parameter_krryn_error_colors() { }

		virtual std::string get_name() { return "krryn_error_colors"; }

		virtual void configure_shader(shader_type a_Type, shader &a_Shader){
			vector4f l_Colors[] = {
				vector4f(1, 1, 1, 1),
				vector4f(0.9, 0, 0, 1)
			};

			a_Shader.bind_float4_array(
				a_Type, 
				get_name(),
				2, 
				l_Colors);
		}
	};

	void load_auto_params(renderer &a_Renderer, shader_catalog &a_Catalog){
		a_Catalog.register_auto_parameter(new shader_auto_parameter_mvp(a_Renderer));
		a_Catalog.register_auto_parameter(new shader_auto_parameter_mv(a_Renderer));
		a_Catalog.register_auto_parameter(new shader_auto_parameter_time());
		a_Catalog.register_auto_parameter(new shader_auto_parameter_krryn_error_colors());
	}
}

shader_catalog::shader_catalog(renderer &a_Renderer) : m_Renderer(a_Renderer){
	set_error_shaders();
	load_auto_params(a_Renderer, *this);
}

bool shader_catalog::is_error_shader(shader &a_Shader){
	return 
		   m_Shaders[m_ErrorShader] == &a_Shader
		|| m_Shaders[m_WarningShader] == &a_Shader;
}

void shader_catalog::set_error_shaders(const std::string &a_Error, const std::string &a_Warning){
	m_ErrorShader = a_Error;
	m_WarningShader = a_Warning;
}

shader *shader_catalog::get_shader(const std::string &a_Filename){
	std::map<std::string, shader*>::iterator l_Shader = m_Shaders.find(a_Filename);

	if(l_Shader == m_Shaders.end()){
		try{
			shader *l_Shader = load_shader(a_Filename);
			m_Shaders[a_Filename] = l_Shader;
			return l_Shader;
		}catch(shader_error &a_Error){
			if(a_Filename != m_ErrorShader && a_Filename != m_WarningShader){
				if(a_Error.m_Type == shader_compile_error){
					return get_shader(m_ErrorShader);
				}else{
					return get_shader(m_WarningShader);
				}
			}else{
				throw std::runtime_error("Can't load error shader");
			}
		}
	}else{
		return l_Shader->second;
	}
}

void shader_catalog::register_auto_parameter(shader_auto_parameter *a_AutoParameter){
	std::string l_Name = a_AutoParameter->get_name();
	std::map<std::string, shader_auto_parameter*>::iterator l_Param = m_AutoParameters.find(l_Name);

	if(l_Param == m_AutoParameters.end()){
		m_AutoParameters[l_Name] = a_AutoParameter;
	}else{
		throw std::runtime_error("Duplicate auto parameter '" + l_Name + "'");
	}
}

shader_auto_parameter* const shader_catalog::get_auto_parameter(const std::string &a_Name){
	std::map<std::string, shader_auto_parameter*>::iterator l_Param = m_AutoParameters.find(a_Name);

	if(l_Param != m_AutoParameters.end()) return l_Param->second;
	else return 0;
}

void shader_catalog::update(){
	std::map<std::string, shader_auto_parameter*>::iterator i = m_AutoParameters.begin();

	for(; i != m_AutoParameters.end(); i++){
		i->second->update();
	}
}